package com.zhao.dota.parser;

import com.zhao.dota.contant.BuildingConst;
import com.zhao.dota.model.MatchPlayerAbility;
import com.zhao.dota.model.MatchPlayerItem;
import com.zhao.dota.model.PlayerProcess;
import lombok.Getter;
import skadistats.clarity.io.Util;
import skadistats.clarity.model.CombatLogEntry;
import skadistats.clarity.model.Entity;
import skadistats.clarity.model.FieldPath;
import skadistats.clarity.model.StringTable;
import skadistats.clarity.processor.entities.Entities;
import skadistats.clarity.processor.entities.OnEntityPropertyChanged;
import skadistats.clarity.processor.entities.UsesEntities;
import skadistats.clarity.processor.gameevents.OnCombatLogEntry;
import skadistats.clarity.processor.reader.OnMessage;
import skadistats.clarity.processor.runner.Context;
import skadistats.clarity.processor.runner.SimpleRunner;
import skadistats.clarity.processor.stringtables.StringTables;
import skadistats.clarity.processor.stringtables.UsesStringTable;
import skadistats.clarity.source.MappedFileSource;
import skadistats.clarity.wire.common.proto.DotaUserMessages;

import java.io.File;
import java.io.IOException;
import java.util.*;

public class DetailParser {

    // constants
    public static final String ABL_PATTERN = "m_hAbilities.*";
    public static final int ABILITY_SLOTS = 24;
    public static final String ABILITY_SLOT_FMT = "m_hAbilities.%04d";
    public static final String CSV_ENCODE = "utf-8";
    public static final String CSV_SEP_ARRAY = ";";
    public static final String CSV_SEP_CAT = ",";
    public static final String CSV_NEWLINE = "\n";
    public static final String ENTITY_INDEX_PROP = "m_pEntity.m_nameStringableIndex";
    public static final String GRP_STR = "CDOTAGamerulesProxy";
    public static final String HERO_PATTERN = "CDOTA_Unit_Hero_.*";
    public static final String ITEM_PATTERN = "m_hItems.*";
    public static final int ITEM_SLOTS = 17;
    public static final String ITEM_SLOT_FMT = "m_hItems.%04d";
    public static final int MAX_PLAYERS = 50;
    public static final String STRTABLE_ENTNAME = "EntityNames";
    public static final String TIME_CUR_PROP = "m_pGameRules.m_fGameTime";
    public static final Float TIME_EPS = 0.01f;
    public static final String TIME_FMT = "%.3f";
    public static final String TIME_PRE_PROP = "m_pGameRules.m_flPreGameStartTime";
    public static final String TIME_SEP = ":";
    public static final String TIME_STA_PROP = "m_pGameRules.m_flGameStartTime";
    public static final String TIME_TRN_PROP = "m_pGameRules.m_flStateTransitionTime";

    private final File file;

    private Float lastTime = -9999.0f;
    private String lastHero = "zippy the frog";

    /**
     * 物品
     */
    @Getter
    private final Map<String, List<MatchPlayerItem>> itemMap;
    @Getter
    private final Map<String, List<MatchPlayerAbility>> abilityMap;

    private final Map<String, Float> itemTimeMap;
    @Getter
    private final Map<String, Integer> towerDamageMap;

    private Integer messageIndex = 0;
    @Getter
    private Float gameTime = 0f;
    @Getter
    private final Map<Integer, List<PlayerProcess>> playerProcessMap = new HashMap<>();

    public DetailParser(File file) {
        this.file = file;
        this.itemMap = new HashMap<>();
        this.itemTimeMap = new HashMap<>();
        this.abilityMap = new HashMap<>();
        this.towerDamageMap = new HashMap<>();
    }

    /**
     * 技能 还没找到。。
     */


    public String getItemSlotNameByInt(int id) {
        return String.format(ITEM_SLOT_FMT, id);
    }

    public String getAbilitySlotNameByInt(int id) {
        return String.format(ABILITY_SLOT_FMT, id);
    }

    public String getEntityNameByHandle(int id, Entities entities, StringTable stringTable) {
        Entity entity = entities.getByHandle(id);
        int index;
        if (entity == null) {
            return null;
        } else {
            index = entity.getProperty(ENTITY_INDEX_PROP);
            return stringTable.getNameByIndex(index);
        }
    }

    public Float getRealGameTimeSeconds(Entities entities) {
        Entity grules = entities.getByDtName(GRP_STR);
        Float gameTime;
        Float startTime;
        Float preGameTime;
        Float transitionTime;
        Float realTime = null;

        // before the match starts, there's CDOTAGamerulesProxy
        if (grules != null) {
            gameTime = grules.getProperty(TIME_CUR_PROP);

            // before the match starts, there's no game "time"
            if (gameTime != null) {
                preGameTime = grules.getProperty(TIME_PRE_PROP);

                // before hero picking and strategy time are finished, the
                //  pre-game countdown is still at 0, i.e. nothing has happened
                //  in the match
                if (preGameTime > TIME_EPS) {
                    startTime = grules.getProperty(TIME_STA_PROP);

                    // time after the clock hits 0:00
                    if (startTime > TIME_EPS) {
                        realTime = gameTime - startTime;
                    }

                    // between the pre-game and 0:00 time of the match, the
                    //  transition time reflects when the match is supposed to
                    //  start (i.e. hit 0:00 on the clock), and gives a good
                    //  approximation of when the match will start. Up to that
                    //  point, the start time is set to 0.
                    else {
                        transitionTime = grules.getProperty(TIME_TRN_PROP);
                        realTime = gameTime - transitionTime;
                    }
                }
            }
        }

        return realTime;
    }

    @UsesEntities
    @UsesStringTable(STRTABLE_ENTNAME)
    public void heroUpdate(Context ctx, Entity hero) {
        // loop declarations
        String abilitySlotName;
        int abilityID;
        String abilityName;


    }

    /**
     * Uses the OnEntityPropertyChanged decorator to automatically update hero status during a replay.
     * <p>
     * This is essentially a listener for changes to hero items during a replay.
     * It uses such events to update hero items and abilities.
     *
     * @param ctx  the {@code Context} of the current runner
     * @param hero the hero {@code Entity} for which to get updates
     * @param fp   not used, present by necessity with the {@code OnEntityPropertyChanged} decorator
     * @throws IOException passed from other code that throws the same
     */
    @OnEntityPropertyChanged(classPattern = HERO_PATTERN, propertyPattern = ITEM_PATTERN)
    public void onItemChange(Context ctx, Entity hero, FieldPath fp) {
        String itemSlotName;
        int itemID;
        String itemName;

        int j;

        Entities entities = ctx.getProcessor(Entities.class);
        StringTable stringTable = ctx.getProcessor(StringTables.class).forName(STRTABLE_ENTNAME);

        // get current game time
        Float gameTime = getRealGameTimeSeconds(entities);

        // get hero abilities and items (if hero has been set yet)
        if (hero != null) {

            // get the hero name and see if this is another update in the same
            //  time. If so, skip it
            String heroName = getEntityNameByHandle(hero.getHandle(), entities, stringTable);
            if (Math.abs(gameTime - lastTime) < TIME_EPS) {
                lastTime = gameTime;
                if (heroName.equals(lastHero)) {
                    return;
                } else {
                    lastHero = heroName;
                }
            }
            lastTime = gameTime;
            // get hero abilities
            Float savedTime = itemTimeMap.get(heroName);
            if (savedTime == null || savedTime < gameTime) {
                // get hero items
                List<MatchPlayerItem> items = new ArrayList<>();
                List<String> itemNames = new ArrayList<>();
                for (j = 0; j < ITEM_SLOTS; j++) {
                    itemSlotName = getItemSlotNameByInt(j);
                    itemID = hero.getProperty(itemSlotName);
                    itemName = getEntityNameByHandle(itemID, entities, stringTable);
                    if (itemName != null) {
                        MatchPlayerItem item = new MatchPlayerItem();
                        item.setId(itemID);
                        item.setName(itemName);
                        item.setLocalizedName(heroName);
                        item.setIndex(j);
                        items.add(item);
                        itemNames.add(itemName);
                    }
                }
                if (heroName.toLowerCase().contains("brewmaster")) {
                    System.out.println(gameTime + " -----\t" + itemNames);
                }
                if (!items.isEmpty()) {
                    itemMap.put(heroName, items);
                    itemTimeMap.put(heroName, gameTime);
                }
            }
        }
    }

    @OnMessage(DotaUserMessages.CDOTAUserMsg_ParticleManager.class)
    public void onMessage(Context ctx, DotaUserMessages.CDOTAUserMsg_ParticleManager message) {
        if (message.getIndex() > messageIndex) {
            messageIndex = message.getIndex();
            Entities entities = ctx.getProcessor(Entities.class);
            gameTime = getRealGameTimeSeconds(entities);
            if (gameTime != null) {
                int time = gameTime.intValue();
                if (gameTime > 0 && !playerProcessMap.containsKey(time)) {
                    List<PlayerProcess> list = getTotal(ctx);
                    if (time % 60 == 0) {
                        playerProcessMap.put(time, list);
                        playerProcessMap.remove(-1);
                    } else {
                        playerProcessMap.put(-1, list);
                    }
                }
            }
        }
    }

    private List<PlayerProcess> getTotal(Context ctx) {
        int team = 0;
        int pos = 0;
        List<PlayerProcess> list = new ArrayList<>();
        for (int idx = 0; idx < 256; idx++) {
            try {
                int newTeam = resolveValue(ctx, idx, team, pos, "m_vecPlayerData.%i.m_iPlayerTeam", "PlayerResource");
                if (newTeam != team) {
                    team = newTeam;
                    pos = 0;
                } else {
                    pos++;
                }
            } catch (Exception e) {
                // when the team resolver throws an exception, this was the last orderNum there was
                break;
            }
            if (team != 2 && team != 3) {
                continue;
            }
            Long steamId = resolveValue(ctx, idx, team, pos, "m_vecPlayerData.%i.m_iPlayerSteamID", "PlayerResource");
            Integer gold = resolveValue(ctx, idx, team, pos, "m_vecDataTeam.%p.m_iTotalEarnedGold", "Data%n");
            Integer xp = resolveValue(ctx, idx, team, pos, "m_vecDataTeam.%p.m_iTotalEarnedXP", "Data%n");
            Integer goldLostToDeath = resolveValue(ctx, idx, team, pos, "m_vecDataTeam.%p.m_iGoldLostToDeath", "Data%n");
            Integer goldSpentOnBuybacks = resolveValue(ctx, idx, team, pos, "m_vecDataTeam.%p.m_iGoldSpentOnBuybacks", "Data%n");
            Integer goldSpentOnConsumables = resolveValue(ctx, idx, team, pos, "m_vecDataTeam.%p.m_iGoldSpentOnConsumables", "Data%n");
            Integer level = resolveValue(ctx, idx, team, pos, "m_vecPlayerTeamData.%i.m_iLevel", "PlayerResource");
            PlayerProcess playerProcess = new PlayerProcess();
            playerProcess.setSteamId(steamId);
            playerProcess.setGold(gold - goldLostToDeath - goldSpentOnBuybacks - goldSpentOnConsumables);
            playerProcess.setXp(xp);
            playerProcess.setLevel(level);
            list.add(playerProcess);
        }
        return list;
    }

    public <T> T resolveValue(Context ctx, int index, int team, int pos, String pattern, String entityName) {
        String fieldPathString = pattern
                .replaceAll("%i", Util.arrayIdxToString(index))
                .replaceAll("%t", Util.arrayIdxToString(team))
                .replaceAll("%p", Util.arrayIdxToString(pos));
        String compiledName = entityName.replaceAll("%n", getTeamName(team));
        Entity entity = getEntity(ctx, compiledName);
        FieldPath fieldPath = entity.getDtClass().getFieldPathForName(fieldPathString);
        return entity.getPropertyForFieldPath(fieldPath);
    }

    private Entity getEntity(Context ctx, String entityName) {
        return ctx.getProcessor(Entities.class).getByDtName(getEngineDependentEntityName(entityName));
    }

    private String getEngineDependentEntityName(String entityName) {
        return "CDOTA_" + entityName;
    }

    private String getTeamName(int team) {
        switch (team) {
            case 2:
                return "Radiant";
            case 3:
                return "Dire";
            default:
                return "";
        }
    }

    @OnEntityPropertyChanged(classPattern = "CDOTABaseAbility|CDOTA_Ability_.*", propertyPattern = "m_iLevel")
    public void onEntityPropertyChanged(Context ctx, Entity e, FieldPath fp) {
        Integer owner = e.getProperty("m_hOwnerEntity");
        Integer NameIndex = e.getProperty("m_pEntity.m_nameStringableIndex");
        Entity ownerEntity = ctx.getProcessor(Entities.class).getByHandle(owner);
        Integer ab_lvl = e.getProperty("m_iLevel");
        if (ab_lvl == 0) {
            return;
        }
        String heroName;
        try {
            heroName = ownerEntity.getDtClass().getDtName();
            if (!heroName.contains("CDOTA_Unit_Hero_")) {
                return;
            }
        } catch (Exception ex) {
            return;
        }
        Integer heroLevel = ownerEntity.getProperty("m_iCurrentLevel");
        StringTable stEntityNames = ctx.getProcessor(StringTables.class).forName("EntityNames");
        String abilityName = stEntityNames.getNameByIndex(NameIndex);
        if (heroName.toLowerCase().contains("doom") && abilityName.contains("sven")) {
            System.out.println("entityName:" + e.getDtClass().getDtName() + ",hero: " + heroName + ", ability:" + abilityName);
        }
        if ("CDOTA_Unit_Hero_AntiMage".equals(heroName)) {
            heroName = "npc_dota_hero_antimage";
        } else {
            heroName = coverHeroName(heroName);
        }


        List<MatchPlayerAbility> abilities = abilityMap.computeIfAbsent(heroName, k -> new ArrayList<>());
        MatchPlayerAbility ability = new MatchPlayerAbility();
        ability.setIndex(abilities.size());
        ability.setHeroName(heroName);
        ability.setName(abilityName);
        ability.setLevel(ab_lvl);
        ability.setHeroLevel(heroLevel);
        abilities.add(ability);
    }

    private String coverHeroName(String heroName) {
        String name = heroName.replace("CDOTA_Unit", "npc_dota");
        name = name.replaceAll("[A-Z]", "_$0");
        name = name.replace("__", "_").toLowerCase();
        return name;
    }

    @OnCombatLogEntry
    public void onCombatLogEntry(CombatLogEntry cle) {
        String attackerName = getAttackerNameCompiled(cle);
        String targetName = getTargetNameCompiled(cle);
        if (cle.getType() == DotaUserMessages.DOTA_COMBATLOG_TYPES.DOTA_COMBATLOG_DAMAGE) {
            // 只统计英雄对塔造成的伤害
            if (!attackerName.contains("hero") || !BuildingConst.BUILDING_NAMES.contains(targetName)) {
                return;
            }
            towerDamageMap.compute(attackerName, (key, oldValue) -> {
                if (oldValue == null) {
                    return cle.getValue();
                }
                return cle.getValue() + oldValue;
            });
        } else if (cle.getType() == DotaUserMessages.DOTA_COMBATLOG_TYPES.DOTA_COMBATLOG_PLAYERSTATS) {
            System.out.printf("[%s] - %s, target: %s\n", attackerName, cle.getValueName(), targetName);
        } else if (cle.getType() == DotaUserMessages.DOTA_COMBATLOG_TYPES.DOTA_COMBATLOG_PURCHASE
                && cle.getValueName().startsWith("item_aghanims_shard")) {

        }
    }

    private String getAttackerNameCompiled(CombatLogEntry cle) {
        return compileName(cle.getAttackerName(), cle.isAttackerIllusion());
    }

    private String getTargetNameCompiled(CombatLogEntry cle) {
        return compileName(cle.getTargetName(), cle.isTargetIllusion());
    }

    private String compileName(String attackerName, boolean isIllusion) {
        return attackerName != null ? attackerName + (isIllusion ? " (illusion)" : "") : "UNKNOWN";
    }

    public void parse() throws IOException {
        SimpleRunner runner = new SimpleRunner(new MappedFileSource(file)).runWith(this);
    }

}
